/*
 * To change this template, choose Tools | Templates
 * and open the template in the editor.
 */
package com.nanosn.coffeemaker.server.restlet.gae;

import com.google.gwt.user.client.rpc.IncompatibleRemoteServiceException;
import com.google.gwt.user.client.rpc.SerializationException;
import com.google.gwt.user.server.rpc.RPC;
import com.google.gwt.user.server.rpc.RPCRequest;
import com.google.gwt.user.server.rpc.RPCServletUtils;
import com.google.gwt.user.server.rpc.SerializationPolicy;
import com.google.gwt.user.server.rpc.SerializationPolicyLoader;
import com.google.gwt.user.server.rpc.SerializationPolicyProvider;
import com.google.gwt.user.server.rpc.UnexpectedException;
import java.io.IOException;
import java.io.InputStream;
import java.net.MalformedURLException;
import java.net.URL;
import java.text.ParseException;
import java.util.HashMap;
import java.util.Map;
import java.util.logging.Level;
import java.util.logging.Logger;
import javax.servlet.ServletContext;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import org.restlet.Context;
import org.restlet.Request;
import org.restlet.Response;
import org.restlet.representation.Representation;
import org.restlet.resource.Post;
import org.restlet.resource.ResourceException;
import org.restlet.resource.ServerResource;

/**
 *
 * @author selkhateeb
 */
public class GwtRpcResource extends ServerResource implements
	SerializationPolicyProvider {

	protected transient ThreadLocal<Request> perThreadRequest;
	protected transient ThreadLocal<Response> perThreadResponse;

	public GwtRpcResource() {
	}

	@Post
	public Representation doPost() {

		try {
			// Store the request & response objects in thread-local storage.
			//
			synchronized (this) {
				validateThreadLocalData();
				perThreadRequest.set(getRequest());
				perThreadResponse.set(getResponse());
			}

			Representation r = processPost();
			return r;

		}
		catch (Throwable e) {
			// Give a subclass a chance to either handle the exception or rethrow it
			//
			doUnexpectedFailure(e);
		}
		finally {
			// null the thread-locals to avoid holding request/response
			//
			perThreadRequest.set(null);
			perThreadResponse.set(null);

		}
		return null;
	}

	/**
	 * Override this method to control what should happen when an exception
	 * escapes the {@link #doPost} method. The default implementation will log the
	 * failure and send a generic failure response to the client.
	 * <p>
	 * An "expected failure" is an exception thrown by a service method that is
	 * declared in the signature of the service method. These exceptions are
	 * serialized back to the client, and are not passed to this method. This
	 * method is called only for exceptions or errors that are not part of the
	 * service method's signature, or that result from SecurityExceptions,
	 * SerializationExceptions, or other failures within the RPC framework.
	 * <p>
	 * Note that if the desired behavior is to both send the GENERIC_FAILURE_MSG
	 * response AND to rethrow the exception, then this method should first send
	 * the GENERIC_FAILURE_MSG response itself (using getThreadLocalResponse), and
	 * then rethrow the exception. Rethrowing the exception will cause it to
	 * escape into the servlet container.
	 *
	 * @param e the exception which was thrown
	 */
	protected void doUnexpectedFailure(Throwable e) {
		try {
			getThreadLocalResponse().release();
		}
		catch (IllegalStateException ex) {
			/*
			 * If we can't reset the request, the only way to signal that something
			 * has gone wrong is to throw an exception from here. It should be the
			 * case that we call the user's implementation code before emitting data
			 * into the response, so the only time that gets tripped is if the object
			 * serialization code blows up.
			 */
			throw new RuntimeException("Unable to report failure", e);
		}
		Context context = getContext();
		GwtRpcRestletUtils.writeResponseForUnexpectedFailure(context,
								     getThreadLocalResponse(),
								     e);
	}

	/**
	 * Gets the <code>HttpServletResponse</code> object for the current call. It
	 * is stored thread-locally so that simultaneous invocations can have
	 * different response objects.
	 */
	protected final Response getThreadLocalResponse() {
		synchronized (this) {
			validateThreadLocalData();
			return perThreadResponse.get();
		}
	}

	private Representation processPost() throws ResourceException,
						    IOException,
						    SerializationException {
		// Read the request fully.
		//
		String requestPayload = readContent(getRequest());

		// Let subclasses see the serialized request.
		//
		onBeforeRequestDeserialized(requestPayload);

		// Invoke the core dispatching logic, which returns the serialized
		// result.
		//
		String responsePayload = processCall(requestPayload);

		// Let subclasses see the serialized response.
		//
		onAfterResponseSerialized(responsePayload);

		// Write the response.
		//
		return writeResponse(getRequest(), getResponse(),
				     responsePayload);
	}

	private Representation writeResponse(Request request,
					     Response response,
					     String responsePayload) throws
		IOException {
		boolean gzipEncode =
			GwtRpcRestletUtils.acceptsGzipEncoding(request) &&
			shouldCompressResponse(request, response,
					       responsePayload);

		return GwtRpcRestletUtils.writeResponse(getContext(), response,
							responsePayload,
							gzipEncode);
	}

	protected boolean shouldCompressResponse(Request request,
						 Response response,
						 String responsePayload) {
		return RPCServletUtils.exceedsUncompressedContentLengthLimit(
			responsePayload);
	}

	private String readContent(Request request) throws ResourceException,
							   IOException {
		return GwtRpcRestletUtils.readContentAsUtf8(getRequest(), true);
	}

	/**
	 * Override this method to examine the serialized version of the request
	 * payload before it is deserialized into objects. The default implementation
	 * does nothing and need not be called by subclasses.
	 *
	 * @param serializedRequest
	 */
	protected void onBeforeRequestDeserialized(String serializedRequest) {
	}

	/**
	 * Override this method to examine the serialized response that will be
	 * returned to the client. The default implementation does nothing and need
	 * not be called by subclasses.
	 *
	 * @param serializedResponse
	 */
	protected void onAfterResponseSerialized(String serializedResponse) {
	}

	/**
	 * Process a call originating from the given request. Uses the
	 * {@link RPC#invokeAndEncodeResponse(Object, java.lang.reflect.Method, Object[])}
	 * method to do the actual work.
	 * <p>
	 * Subclasses may optionally override this method to handle the payload in any
	 * way they desire (by routing the request to a framework component, for
	 * instance). The {@link HttpServletRequest} and {@link HttpServletResponse}
	 * can be accessed via the {@link #getThreadLocalRequest()} and
	 * {@link #getThreadLocalResponse()} methods.
	 * </p>
	 * This is public so that it can be unit tested easily without HTTP.
	 *
	 * @param payload the UTF-8 request payload
	 * @return a string which encodes either the method's return, a checked
	 *         exception thrown by the method, or an
	 *         {@link IncompatibleRemoteServiceException}
	 * @throws SerializationException if we cannot serialize the response
	 * @throws UnexpectedException if the invocation throws a checked exception
	 *           that is not declared in the service method's signature
	 * @throws RuntimeException if the service method throws an unchecked
	 *           exception (the exception will be the one thrown by the service)
	 */
	public String processCall(String payload) throws SerializationException {
		try {
//			RPCRequest rpcRequest = RPC.decodeRequest(payload, this.
//				getClass(), this);
			RPCRequest rpcRequest = RPC.decodeRequest(payload, this.
				getClass(), this);
			System.out.println(rpcRequest.getMethod().
				getDeclaringClass());
			onAfterRequestDeserialized(rpcRequest);
			return RPC.invokeAndEncodeResponse(this, rpcRequest.
				getMethod(),
							   rpcRequest.
				getParameters(), rpcRequest.
				getSerializationPolicy(),
							   rpcRequest.getFlags());
		}
		catch (IncompatibleRemoteServiceException ex) {
			Logger.getLogger(GwtRpcRepresentation.class.getClass().
				getName()).log(Level.SEVERE,
					       "An IncompatibleRemoteServiceException was thrown while processing this call.",
					       ex);
			return RPC.encodeResponseForFailure(null, ex);
		}
	}

	/**
	 * Override this method to examine the deserialized version of the request
	 * before the call to the servlet method is made. The default implementation
	 * does nothing and need not be called by subclasses.
	 *
	 * @param rpcRequest
	 */
	protected void onAfterRequestDeserialized(RPCRequest rpcRequest) {
	}

	public final SerializationPolicy getSerializationPolicy(
		String moduleBaseURL,
		String strongName) {

		SerializationPolicy serializationPolicy = getCachedSerializationPolicy(
			moduleBaseURL, strongName);
		if (serializationPolicy != null) {
			return serializationPolicy;
		}

		serializationPolicy = doGetSerializationPolicy(
			getThreadLocalRequest(),
			moduleBaseURL,
			strongName);

		if (serializationPolicy == null) {
			// Failed to get the requested serialization policy; use the default
			Logger.getLogger(GwtRpcRepresentation.class.getClass().
				getName()).log(Level.WARNING,
					       "WARNING: Failed to get the SerializationPolicy '" +
					       strongName + "' for module '" +
					       moduleBaseURL +
					       "'; a legacy, 1.3.3 compatible, serialization policy will be used.  You may experience SerializationExceptions as a result");
			serializationPolicy =
				RPC.getDefaultSerializationPolicy();
		}

		// This could cache null or an actual instance. Either way we will not
		// attempt to lookup the policy again.
		putCachedSerializationPolicy(moduleBaseURL, strongName,
					     serializationPolicy);

		return serializationPolicy;
	}
	/**
	 * A cache of moduleBaseURL and serialization policy strong name to
	 * {@link SerializationPolicy}.
	 */
	private final Map<String, SerializationPolicy> serializationPolicyCache =
		new HashMap<String, SerializationPolicy>();

	private SerializationPolicy getCachedSerializationPolicy(
		String moduleBaseURL, String strongName) {
		synchronized (serializationPolicyCache) {
			return serializationPolicyCache.get(moduleBaseURL +
							    strongName);
		}
	}

	/**
	 * Initializes the perThreadRequest and perThreadResponse fields if they are
	 * null. This will occur the first time they are accessed after an instance of
	 * this class is constructed or deserialized. This method should be called
	 * from within a 'synchronized(this) {}' block in order to ensure that only
	 * one thread creates the objects.
	 */
	private void validateThreadLocalData() {
		if (perThreadRequest == null) {
			perThreadRequest = new ThreadLocal<Request>();
		}
		if (perThreadResponse == null) {
			perThreadResponse =
				new ThreadLocal<Response>();
		}
	}

	/**
	 * Gets the <code>HttpServletRequest</code> object for the current call. It is
	 * stored thread-locally so that simultaneous invocations can have different
	 * request objects.
	 */
	protected final Request getThreadLocalRequest() {
		synchronized (this) {
			validateThreadLocalData();
			return perThreadRequest.get();
		}
	}

	/**
	 * Gets the {@link SerializationPolicy} for given module base URL and strong
	 * name if there is one.
	 *
	 * Override this method to provide a {@link SerializationPolicy} using an
	 * alternative approach.
	 *
	 * @param request the HTTP request being serviced
	 * @param moduleBaseURL as specified in the incoming payload
	 * @param strongName a strong name that uniquely identifies a serialization
	 *          policy file
	 * @return a {@link SerializationPolicy} for the given module base URL and
	 *         strong name, or <code>null</code> if there is none
	 */
	protected SerializationPolicy doGetSerializationPolicy(
		Request request, String moduleBaseURL,
		String strongName) {
		return loadSerializationPolicy(this, request, moduleBaseURL,
					       strongName);
	}

	/**
	 * Used by HybridServiceServlet.
	 */
	static SerializationPolicy loadSerializationPolicy(
		ServerResource resource, Request request,
		String moduleBaseURL,
		String strongName) {
		// The request can tell you the path of the web app relative to the
		// container root.
		String contextPath = request.getResourceRef().getPath();

		String modulePath = null;
		if (moduleBaseURL != null) {
			try {
				modulePath = new URL(moduleBaseURL).getPath();
			}
			catch (MalformedURLException ex) {
				// log the information, we will default
				resource.getLogger().log(Level.SEVERE,
							 "Malformed moduleBaseURL: " +
							 moduleBaseURL, ex);
			}
		}
		SerializationPolicy serializationPolicy = null;

		/*
		 * Check that the module path must be in the same web app as the servlet
		 * itself. If you need to implement a scheme different than this, override
		 * this method.
		 */
//		if (modulePath == null || !modulePath.startsWith(contextPath)) {
//			String message =
//				"ERROR: The module path requested, " +
//				modulePath +
//				", is not in the same web application as this servlet, " +
//				contextPath +
//				".  Your module may not be properly configured or your client and server code maybe out of date.";
//			resource.getLogger().log(Level.SEVERE, message);
//		}
//		else {
		// Strip off the context path from the module base URL. It should be a
		// strict prefix.
		System.out.println(modulePath);
		System.out.println(contextPath);
		String contextRelativePath = modulePath;//.substring(
//				contextPath.length());

		String serializationPolicyFilePath =
			SerializationPolicyLoader.getSerializationPolicyFileName(
			contextRelativePath +
			strongName);

		// Open the RPC resource file and read its contents.
		ServletContext sc = (ServletContext) resource.getContext().
			getAttributes().
			get("org.restlet.ext.servlet.ServletContext");

		InputStream is = sc.getResourceAsStream(
			serializationPolicyFilePath);
		try {
			if (is != null) {
				try {
					serializationPolicy =
						SerializationPolicyLoader.
						loadFromStream(is,
							       null);
				}
				catch (ParseException e) {
					resource.getLogger().log(
						Level.SEVERE, "ERROR: Failed to parse the policy file '" +
							      serializationPolicyFilePath +
							      "'", e);
				}
				catch (IOException e) {
					resource.getLogger().log(
						Level.SEVERE, "ERROR: Could not read the policy file '" +
							      serializationPolicyFilePath +
							      "'", e);
				}
			}
			else {
				String message =
					"ERROR: The serialization policy file '" +
					serializationPolicyFilePath +
					"' was not found; did you forget to include it in this deployment?";
				resource.getLogger().log(Level.SEVERE,
							 message);
			}
		}
		finally {
			if (is != null) {
				try {
					is.close();
				}
				catch (IOException e) {
					// Ignore this error
					}
			}
		}
//		}

		return serializationPolicy;
	}

	private void putCachedSerializationPolicy(String moduleBaseURL,
						  String strongName,
						  SerializationPolicy serializationPolicy) {
		synchronized (serializationPolicyCache) {
			serializationPolicyCache.put(moduleBaseURL + strongName,
						     serializationPolicy);
		}
	}
}

